05. Numpy-数组的创建与访问

本章概要

  • 学习材料:一维收益数组、二维多资产矩阵与切片副本。
  • 本章任务:运行 lst-numpy-bool-indexlst-numpy-copylst-numpy-reshape,完成筛选、复制与变形。
  • 完成后你将得到:筛选数组、原数组/副本对照及 reshape 后 shape。
  • 自我检查:检查元素数守恒且副本修改不污染原数组;形状不兼容时先检查原因。
  • 拓展练习:把同一访问和复制规则拓展应用到另一收益矩阵。

NumPy:Python科学计算的基石

  • NumPy(Numerical Python)是Python科学计算的基础包
  • 提供高性能的多维数组对象和相关数值计算工具
  • 是Pandas、Matplotlib、Scikit-learn等库的底层基础
  • 在金融数据分析中地位无可替代

NumPy数组 vs Python列表:核心差异

特性 Python列表 NumPy数组
内存存储 分散存储对象引用 连续内存存储
元素类型 可以不同 必须相同
性能 解释执行,较慢 编译优化,极快
向量化 不支持 原生支持
内存效率 较低 较高(紧凑存储)

为什么NumPy更快?

  • 连续内存:缓存友好,减少内存访问延迟
  • 类型一致:避免动态类型检查的开销
  • 向量化操作:底层C/Fortran实现,利用SIMD指令
  • 广播机制:自动对齐不同形状的数组,无需手写循环

运行前预测|平台任务代码

  • 输入预测:运行前先写出 stock1stock2stock_con 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到stock_con 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务代码”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务代码

# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
# 已知股价数据
stock1=np.array([22.91,22.89,23.38,23.09,22.90])
stock2=np.array([41.14,40.99,41.29,40.81,40.97])  # 创建NumPy数组stock2
# 1. 查看股票1的属性值情况,并输出。
print(stock1.shape)
# 2. 输出股票2周二的股价
print(stock2[1])
# 3. 连接股票1与股票2的数据(按照2X5的方式连接),并输出
stock_con=np.concatenate(([stock1],[stock2]),axis=0)
print(stock_con)  # 输出两只股票收盘价合并后的2×5矩阵

任务复盘|平台任务代码

运行后核对:核对 stock1stock2stock_con 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:只改变一个关键输入或业务场景,先预测输出如何变化,再运行验证并解释变化原因。

np.array():从列表创建数组

Listing 1: 使用np.array()创建一维数组
import numpy as np

# 从Python列表创建一维NumPy数组
# 存储两只股票一周的收盘价
stock1 = np.array([22.91, 22.89, 23.38, 23.09, 22.90])
stock2 = np.array([41.14, 40.99, 41.29, 40.81, 40.97])

print(f'股票1收盘价: {stock1}')
print(f'股票2收盘价: {stock2}')
print(f'数据类型: {stock1.dtype}')  # float64:双精度浮点数
股票1收盘价: [22.91 22.89 23.38 23.09 22.9 ]
股票2收盘价: [41.14 40.99 41.29 40.81 40.97]
数据类型: float64

np.array()的关键参数

  • object:输入数据(列表、元组或其他数组)
  • dtype:指定数据类型(如np.float64np.int32
  • copy:是否复制数据(默认True)

金融数据类型选择

  • float64:双精度,金融计算推荐(价格、收益率)
  • float32:节省内存,适合大规模数据
  • int64:整数类型(成交量、日期索引)

创建二维数组:多只股票数据

Listing 2: 二维NumPy数组的创建与应用
# 从嵌套列表创建二维数组
# 外层列表的每个元素是一行
stocks_2d = np.array([
    [22.91, 22.89, 23.38, 23.09, 22.90],  # 股票1的5日收盘价
    [41.14, 40.99, 41.29, 40.81, 40.97]   # 股票2的5日收盘价
])

print(f'二维数组形状: {stocks_2d.shape}')  # (2, 5):2行5列
print(f'行数: {stocks_2d.shape[0]}')       # 2
print(f'列数: {stocks_2d.shape[1]}')       # 5
二维数组形状: (2, 5)
行数: 2
列数: 5

数组的关键属性

Listing 3: 查看NumPy数组的核心属性
展开实现代码
# .ndim:维度数(秩)
print(f'数组维度: {stocks_2d.ndim}')   # 2(二维数组)

# .size:元素总数
print(f'数组大小: {stocks_2d.size}')   # 10(2×5=10)

# .dtype:数据类型
print(f'数据类型: {stocks_2d.dtype}')  # float64
数组维度: 2
数组大小: 10
数据类型: float64

shape属性的维度含义

  • 一维 (5,):单只股票的时间序列
  • 二维 (2, 5):多只股票的时间序列
  • 三维 (2, 3, 5):多市场 × 多股票 × 多日(面板数据)

一维数组索引:访问单个元素

Listing 4: 一维数组的单元素索引
# 索引从0开始:0=周一,1=周二,...,4=周五
tuesday_price = stock2[1]  # 获取周二的股价
print(f'股票2周二股价: {tuesday_price:.2f}元')  # 40.99元

# 负数索引从末尾开始
friday_price = stock2[-1]  # 最后一个元素(周五)
print(f'股票2周五股价: {friday_price:.2f}元')   # 40.97元
股票2周二股价: 40.99元
股票2周五股价: 40.97元

一维数组切片:[start:end:step]

Listing 5: 一维数组的切片操作
# 切片语法:[start:end:step]
# start包含,end不包含
print(f'前3天价格: {stock2[0:3]}')   # 索引0,1,2

# 省略start默认从0开始,省略end到末尾
print(f'后2天价格: {stock2[-2:]}')   # 倒数2个元素

# 步长为2,每隔一个取一个
print(f'隔日价格: {stock2[::2]}')    # 索引0,2,4
前3天价格: [41.14 40.99 41.29]
后2天价格: [40.81 40.97]
隔日价格: [41.14 41.29 40.97]

切片规则start包含,end不包含;负数索引从末尾计数

布尔索引:条件筛选

Listing 6: 使用布尔索引筛选数据
# stock2 > 41.0 返回布尔数组
mask = stock2 > 41.0
print(f'布尔掩码: {mask}')

# 用布尔数组作为索引,返回True对应的元素
high_price_days = stock2[mask]
print(f'股价>41元的交易日: {high_price_days}')
布尔掩码: [ True False  True False False]
股价>41元的交易日: [41.14 41.29]
  • 比较运算符逐元素比较,返回布尔数组
  • 布尔数组作为索引,筛选满足条件的元素

二维数组索引:array[row, col]

Listing 7: 二维NumPy数组的多维索引
# 获取单个元素:array[行索引, 列索引]
element = stocks_2d[0, 2]  # 第1行第3列
print(f'stocks_2d[0, 2] = {element}')  # 23.38

# 获取整行:用 : 表示选择所有列
row_1 = stocks_2d[1, :]  # 第2行所有列
print(f'股票2所有交易日: {row_1}')

# 获取整列:用 : 表示选择所有行
col_3 = stocks_2d[:, 2]  # 所有行的第3列
print(f'周三所有股票价格: {col_3}')
stocks_2d[0, 2] = 23.38
股票2所有交易日: [41.14 40.99 41.29 40.81 40.97]
周三所有股票价格: [23.38 41.29]

二维数组切片:获取子矩阵

Listing 8: 二维数组的切片提取子矩阵
# 所有行,第2-4列(索引1,2,3)
sub_matrix = stocks_2d[:, 1:4]
print(f'周二到周四的价格:\n{sub_matrix}')
周二到周四的价格:
[[22.89 23.38 23.09]
 [40.99 41.29 40.81]]

金融应用场景

  • 单只股票时间序列arr[stock_id, :]
  • 单日所有股票arr[:, day_id]
  • 特定时间段arr[:, start:end]

concatenate():数组拼接

Listing 9: 使用concatenate拼接NumPy数组
# axis=0:沿行方向拼接(垂直堆叠)
stock_con = np.concatenate(([stock1], [stock2]), axis=0)
print(f'垂直拼接结果:\n{stock_con}')
print(f'形状: {stock_con.shape}')  # (2, 5)

# axis=0:一维数组首尾相接
prices_aug = np.array([22.95, 23.02])
stock2_aug = np.concatenate((stock2, prices_aug), axis=0)
print(f'\n扩充后的股票2: {stock2_aug}')
垂直拼接结果:
[[22.91 22.89 23.38 23.09 22.9 ]
 [41.14 40.99 41.29 40.81 40.97]]
形状: (2, 5)

扩充后的股票2: [41.14 40.99 41.29 40.81 40.97 22.95 23.02]

vstack与hstack:快捷拼接函数

Listing 10: vstack和hstack的使用
展开实现代码
# vstack:垂直堆叠(等价于concatenate axis=0)
stock_vstack = np.vstack((stock1, stock2))
print(f'垂直堆叠:\n{stock_vstack}')

# hstack:水平连接(等价于concatenate axis=1)
stock_hstack = np.hstack((stock1, stock2))
print(f'水平连接: {stock_hstack}')
垂直堆叠:
[[22.91 22.89 23.38 23.09 22.9 ]
 [41.14 40.99 41.29 40.81 40.97]]
水平连接: [22.91 22.89 23.38 23.09 22.9  41.14 40.99 41.29 40.81 40.97]
函数 功能 等价操作
concatenate 通用拼接(指定axis)
vstack 垂直堆叠 concatenate(..., axis=0)
hstack 水平连接 concatenate(..., axis=1)

数组分割:hsplit与vsplit

Listing 11: NumPy数组的分割操作
# hsplit:水平分割(按列切分)
stocks_split = np.hsplit(stocks_2d, [2, 4])
print('按列分割结果:')
for i, part in enumerate(stocks_split):
    print(f'  部分{i+1}: 形状{part.shape}')

# vsplit:垂直分割(按行切分)
stocks_vsplit = np.vsplit(stocks_2d, 2)
print(f'\n按行分割:')
for i, part in enumerate(stocks_vsplit):
    print(f'  部分{i+1}: {part}')
按列分割结果:
  部分1: 形状(2, 2)
  部分2: 形状(2, 2)
  部分3: 形状(2, 1)

按行分割:
  部分1: [[22.91 22.89 23.38 23.09 22.9 ]]
  部分2: [[41.14 40.99 41.29 40.81 40.97]]

视图(View)与副本(Copy)

  • 视图:共享原数组的内存,修改视图会影响原数组
  • 副本:拥有独立的内存,修改副本不影响原数组
  • 理解两者区别对避免意外数据修改至关重要

切片返回视图,修改会影响原数组

Listing 12: 切片返回视图的演示
original = np.array([1, 2, 3, 4, 5])

# 切片返回视图(共享内存)
view = original[1:4]
view[0] = 999  # 修改视图
print(f'修改视图后,原数组: {original}')  # [1, 999, 3, 4, 5]
修改视图后,原数组: [  1 999   3   4   5]
  • 切片操作 [1:4] 返回的是原数组的视图
  • 视图与原数组共享同一块内存

.copy()创建独立副本

Listing 13: .copy()方法创建独立副本
original = np.array([1, 2, 3, 4, 5])

# .copy()创建副本(独立内存)
copy = original[1:4].copy()
copy[0] = 888  # 修改副本
print(f'修改副本后,原数组: {original}')  # [1, 2, 3, 4, 5]

# Fancy Indexing也返回副本
fancy = original[[0, 2, 4]]
fancy[0] = 777
print(f'Fancy索引修改后,原数组: {original}')  # [1, 2, 3, 4, 5]
修改副本后,原数组: [1 2 3 4 5]
Fancy索引修改后,原数组: [1 2 3 4 5]

视图与副本规则总结

操作 返回类型 是否共享内存
切片 [start:end] 视图 ✅ 是
Fancy索引 [[0,2]] 副本 ❌ 否
布尔索引 [mask] 副本 ❌ 否
.copy() 副本 ❌ 否

最佳实践:不确定时,使用 .copy() 避免意外修改

reshape:改变数组形状

Listing 14: reshape改变数组形状
展开实现代码
arr_1d = np.arange(10)  # 创建0-9的一维数组
print(f'一维数组: {arr_1d}')

# reshape(2, 5):转为2行5列,元素总数不变
arr_2x5 = arr_1d.reshape(2, 5)
print(f'\n2×5数组:\n{arr_2x5}')

# -1表示自动计算该维度
arr_5x2 = arr_1d.reshape(-1, 2)  # 自动计算行数=5
print(f'\n5×2数组:\n{arr_5x2}')
一维数组: [0 1 2 3 4 5 6 7 8 9]

2×5数组:
[[0 1 2 3 4]
 [5 6 7 8 9]]

5×2数组:
[[0 1]
 [2 3]
 [4 5]
 [6 7]
 [8 9]]

展平与转置

Listing 15: 数组展平与转置操作
展开实现代码
arr_2x5 = np.arange(10).reshape(2, 5)

# ravel():展平为一维(可能返回视图)
flat_ravel = arr_2x5.ravel()
print(f'ravel结果: {flat_ravel}')

# flatten():展平为一维(总是返回副本)
flat_flatten = arr_2x5.flatten()
print(f'flatten结果: {flat_flatten}')

# .T:转置(行列互换)
transpose = arr_2x5.T  # 2×5 → 5×2
print(f'\n转置后 {transpose.shape}:\n{transpose}')
ravel结果: [0 1 2 3 4 5 6 7 8 9]
flatten结果: [0 1 2 3 4 5 6 7 8 9]

转置后 (5, 2):
[[0 5]
 [1 6]
 [2 7]
 [3 8]
 [4 9]]

广播(Broadcasting)机制

广播允许不同形状的数组进行算术运算,NumPy会自动扩展较小的数组。

广播规则

  1. 尾部(最右边)开始比较维度
  2. 维度相同或其中一个为1则兼容
  3. 缺失的维度视为1

广播示例:标量与数组

Listing 16: 标量与数组的广播运算
prices = np.array([10, 20, 30, 40, 50])
discount = 0.9  # 打9折

# 标量自动"广播"到数组的每个元素
discounted_prices = prices * discount
print(f'原价: {prices}')
print(f'折扣后: {discounted_prices}')
原价: [10 20 30 40 50]
折扣后: [ 9. 18. 27. 36. 45.]
  • 标量 0.9 被自动扩展为 [0.9, 0.9, 0.9, 0.9, 0.9]
  • 等价于逐元素相乘,但无需手写循环

广播示例:不同形状数组运算

Listing 17: 不同形状数组的广播运算
base_prices = np.array([10, 20, 30])        # 形状 (3,)
adjustments = np.array([[1.1], [0.9], [1.0]])  # 形状 (3, 1)

# 广播:(3,) → (3,3),(3,1) → (3,3)
result = base_prices * adjustments
print(f'调整后的价格矩阵:\n{result}')
调整后的价格矩阵:
[[11. 22. 33.]
 [ 9. 18. 27.]
 [10. 20. 30.]]

广播应用:收益率标准化

Listing 18: 利用广播机制标准化收益率
returns = np.array([[0.05, 0.03, 0.04],
                    [0.06, 0.02, 0.05]])  # 2只股票,3个时间点

# keepdims=True 保持维度,便于广播
mean = returns.mean(axis=1, keepdims=True)  # (2,1)
std = returns.std(axis=1, keepdims=True)    # (2,1)

# 标准化:(收益率 - 均值) / 标准差
normalized = (returns - mean) / std
print(f'标准化后的收益率:\n{normalized}')
标准化后的收益率:
[[ 1.22474487 -1.22474487  0.        ]
 [ 0.98058068 -1.37281295  0.39223227]]

向量化 vs 循环:性能对比

Listing 19: 向量化操作的性能优势
展开实现代码
import time

n = 1000000  # 100万个数据点
prices = np.random.randn(n) * 10 + 100  # 模拟股价数据

# 方法1:Python循环(慢)
start = time.time()
returns_loop = []
for i in range(1, len(prices)):
    ret = (prices[i] - prices[i-1]) / prices[i-1]
    returns_loop.append(ret)
loop_time = time.time() - start

# 方法2:NumPy向量化(快)
start = time.time()
returns_vec = (prices[1:] - prices[:-1]) / prices[:-1]
vec_time = time.time() - start

print(f'循环时间: {loop_time:.4f}秒')
print(f'向量化时间: {vec_time:.4f}秒')
print(f'性能提升: {loop_time/vec_time:.1f}倍')
循环时间: 0.3990秒
向量化时间: 0.0022秒
性能提升: 178.7倍

本章核心要点回顾

  • 创建数组np.array() 从列表创建,支持多维
  • 索引切片[start:end:step],二维用 [row, col]
  • 布尔索引:条件筛选数组元素
  • 拼接分割concatenate/vstack/hstackhsplit/vsplit
  • 视图与副本:切片返回视图,.copy() 创建独立副本
  • 广播机制:不同形状数组自动对齐运算

随堂练习

  • 问题 1|需要准备哪些数据?:一维收益数组、二维多资产矩阵与切片副本。
  • 问题 2|需要完成哪些操作?:运行 lst-numpy-bool-indexlst-numpy-copylst-numpy-reshape,完成筛选、复制与变形。
  • 问题 3|应得到哪些结果?:筛选数组、原数组/副本对照及 reshape 后 shape。
  • 问题 4|怎样确认结果可靠?:检查元素数守恒且副本修改不污染原数组;形状不兼容时先检查原因。
  • 问题 5|换一个情境,怎样继续应用?:把同一访问和复制规则拓展应用到另一收益矩阵。
  • 作答提示:请依次写清所用数据、分析过程、所得结果、核对方法和拓展思考。课程所需数据见前言中的下载入口;教学平台固定题按页面说明完成。

教师参考解答|答案与说明 1

  • 补充练习|创建、访问、复制与变形

教师参考解答|代码 1

展开代码(代码区可独立滚动)
import numpy as np  # 导入数组计算库
returns=np.array([[0.01,-0.02,0.03],[0.00,0.04,-0.01]],dtype=float)  # 创建二维多资产收益矩阵
positive=returns[returns>0]  # 用布尔索引访问全部正收益
first_asset=returns[0,1:]  # 用二维切片访问首资产后两期
independent=returns.copy()  # 创建不会污染原矩阵的独立副本
independent[0,0]=0.99  # 修改副本以检验复制边界
reshaped=returns.reshape(3,2)  # 在元素数不变时变形
assert positive.tolist()==[0.01,0.03,0.04]  # 检查布尔访问结果
assert first_asset.tolist()==[-0.02,0.03]  # 检查二维切片结果
assert returns[0,0]==0.01 and independent[0,0]==0.99  # 检查副本修改不污染原数组
assert reshaped.shape==(3,2) and reshaped.size==returns.size  # 检查变形前后元素守恒
print({'筛选':positive.tolist(),'副本独立':True,'变形':reshaped.shape})  # 输出可见核对结果

教师参考解答|答案与说明 2

  • 解释与拓展应用答案:布尔索引返回满足条件的元素,.copy() 隔离修改,reshape 只在元素数守恒时成立;拓展应用到另一收益矩阵时复用同样三道检查。

  • 所用数据与字段:二维价格表 shape=(2,3),浮点类型

  • 参考代码或推导(平台保护块之外):

教师参考解答|代码 2

展开代码(代码区可独立滚动)
import numpy as np  # 导入数组库
new_returns=np.array([[0.02,-0.01],[0.03,0.00],[-0.02,0.01]],dtype=float)  # 创建新案例收益矩阵
selected=new_returns[new_returns>=0.02]  # 按同一规则筛选新案例数据
new_copy=new_returns.copy()  # 建立新案例数据副本
new_copy[0,0]=-1.0  # 修改新案例副本以执行污染测试
new_shape=new_returns.reshape(2,3)  # 按元素守恒规则变形
assert selected.tolist()==[0.02,0.03]  # 检查新案例筛选结果
assert new_returns[0,0]==0.02 and new_shape.size==new_returns.size  # 检查复制与变形边界
print({'新案例筛选':selected.tolist(),'新案例形状':new_shape.shape})  # 输出新案例结果

教师参考解答|答案与说明 3

  • 参考结果:筛选值、副本独立布尔值与 reshape 后 shape
  • 边界 / 局限:行列的含义应保持一致;元素总数不一致时不要改变数组形状
  • 常见错误:把视图当副本;形状乘积不相等;布尔条件方向写反